package edu.arizona.sista.learn.activity.model;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import edu.arizona.sista.learn.activity.model.args.PredActor;
import edu.arizona.sista.learn.activity.model.args.PredAgent;
import edu.arizona.sista.learn.activity.model.args.PredArg;
import edu.arizona.sista.learn.activity.model.args.PredLocation;
import edu.arizona.sista.learn.activity.model.args.PredObject;
import edu.arizona.sista.learn.activity.util.KnownPredicates;


public class Predicate extends PredArg{

	protected String _name;
	protected PredArg[] _args;
	final protected Predicate[] _supers;
	final protected Predicate[] _composedOf;
	final protected Conjunction _definition;
	final protected Predicate[] _inferredFrom;
	
	public Predicate(String name){
		this(name, new PredArg[0]);
	}
	
	public Predicate(String name, List<? extends PredArg> args){
		this(name, args.toArray(new PredArg[0]));
	}
	
	public Predicate(String name, PredArg... args){
		this(name, (Predicate[])null, null, (Conjunction)null, null, args);
	}
	
	public Predicate(String name, Predicate[] supers, Predicate[] composedOf, 
			Relation[] definition, Predicate[] inferredFrom, PredArg... args){
		this(name,supers,composedOf,new Conjunction(definition),inferredFrom,args);
	}
	
	public Predicate(String name, Predicate[] supers, Predicate[] composedOf, 
			Conjunction definition, Predicate[] inferredFrom, PredArg... args){
		_type = "predicate";
		_name = name;
		_args = args;
		_supers = supers;
		_composedOf = composedOf;
		_definition = definition;
		_inferredFrom = inferredFrom;
	}
	
	public Predicate(String name, List<? extends Predicate> supers, 
			List<? extends Predicate> composedOf, List<Relation> definition, 
			List<? extends Predicate> inferredFrom, PredArg...args){
		this(name, supers, composedOf, new Conjunction(definition), inferredFrom, args);
	}
	
	public Predicate(String name, List<? extends Predicate> supers, 
			List<? extends Predicate> composedOf, Conjunction definition, 
			List<? extends Predicate> inferredFrom, PredArg...args){
		this(name, supers.toArray(new Predicate[0]), composedOf.toArray(new Predicate[0]), 
				definition, inferredFrom.toArray(new Predicate[0]), args);
	}
	
//	@Deprecated
//	public Predicate renameArgs(String...argNames){
//		for( int i=0; i<argNames.length && i<numArgs(); i++)
//			getArg(i).setVariableName(argNames[i]);
//		return this;
//	}
	
	@Override
	public Bindings getValidBinding(PredArg testBinding, Bindings currentBindings){
		if( testBinding instanceof Predicate )
			return getValidBinding((Predicate)testBinding, currentBindings);
		else
			return super.getValidBinding(testBinding, currentBindings);
	}
	
	public Bindings getValidBinding(Predicate predicate, Bindings currentBindings){

		if( !_name.equals(predicate.getName())  ||
				numArgs() != predicate.numArgs() )
			return null;
		
		Bindings tempBindings;
		if( currentBindings == null )
			tempBindings = new Bindings();
		else
			tempBindings = new Bindings(currentBindings);
		
		for( int i=0; i<numArgs(); i++){

			tempBindings = getArg(i).getValidBinding(predicate.getArg(i), tempBindings);

			if( tempBindings == null){
				return null;
			}
		}
		
		return tempBindings;
	}
	
	public void bindArgument(String variableName, String value) {
		for (PredArg arg : _args) {
			if (arg.getVariableName().equals(variableName)) {
				arg.bind(value);
			}
		}
	}
	
	public void bindArguments(Bindings newBindings) {
		for (int i=0; i<_args.length; i++) {

			if ( newBindings.containsKey(_args[i]) ) {
				// We want to use whichever type was in the binding list
				//  because it could be more specific than our args (ie an Agent)
				_args[i] = newBindings.getKey(_args[i]).copy();
				_args[i].bind( newBindings.get(_args[i]));
			} else {
//				throw new RuntimeException( "No binding in Bindings for arg: "+_args[i] );
//				System.out.println("Warning: No binding in Bindings for arg: "+_args[i]);
				
				if( _args[i].getCannotEqual() != null ){

					for( int j=0; j<_args[i].getCannotEqual().length; j++ ){
						if( newBindings.containsKey(new PredArg(_args[i].getCannotEqual()[j],null))){
							_args[i].getCannotEqual()[j] = newBindings.get(new PredArg(_args[i].getCannotEqual()[j],null));
						} else
							throw new RuntimeException( "No positive OR negative bindings in Bindings for arg: "+_args[i]);
					}
					_args[i].setVariableName(null);
				}
			}
		}

	}
	
//	public boolean bindableTo(Predicate predicate, Bindings currentBindings){
//		return getValidBinding(predicate, currentBindings) != null;
//	}
	
	public int numArgs(){
		return _args.length;
	}
	
	public PredArg[] getArgs(){
		return _args;
	}
	
	public void setName(String name){
		_name = name;
	}
	
	public String getName(){
		return _name;
	}
	
	public void setArgs(PredObject[] args){
		_args = args;
	}
	
	public void setArg(int index, PredObject arg){
		if( index < numArgs() )
			_args[index] = arg;
	}
	
	public PredArg getArg(int index){
		if( index < numArgs() )
			return _args[index];
		else
			return null;
	}
	
	public Predicate[] getSupers(){
		return _supers;
	}
	
	public Predicate[] getComposedOf(){
		return _composedOf;
	}
	
	public Conjunction getDefinition(){
		return _definition;
	}
	
	public Predicate[] getInferredFrom(){
		return _inferredFrom;
	}
	
	@Deprecated
	public Predicate copy(){
		PredArg[] argsCopy = Arrays.copyOf(_args, _args.length);
		for( int i=0; i<argsCopy.length; i++ )
			argsCopy[i] = argsCopy[i].copy(); 
		Predicate copy = new Predicate(
				_name, 
				_supers,
				_composedOf,
				_definition,
				_inferredFrom, 
				argsCopy);
			
		return copy;
	}
	
	public Predicate recastCopy(PredArg... replacementArgs) {
		if( numArgs() != replacementArgs.length )
			throw new RuntimeException("Invalid argument recast: wrong number of args");
	
		Map<PredArg,PredArg> replacementMap = new HashMap<PredArg,PredArg>();
		for( int i=0; i<numArgs(); i++ )
			if( getArg(i) instanceof Predicate || replacementArgs[i] instanceof Predicate )
				throw new RuntimeException("Not yet implemented: Predicate with Predicate arguments.");
			else
			if(  !( getArg(i).getClass().isInstance(replacementArgs[i]) )  )
				throw new RuntimeException("Invalid argument recast: "+getArg(i)+" to "+replacementArgs[i] );
			else
				replacementMap.put(getArg(i), replacementArgs[i]);
		
		return recastCopy(replacementMap);
	}
	
	/*
	 * Copy this Predicate, but replace its args with these. Necessary
	 * for when we need a more restrictive version of the predicate
	 * eg. have(agent:?x, object?y) instead of have(object:?x, object?y)
	 * Also used for renaming args.
	 */
	public Predicate recastCopy(Map<PredArg,PredArg> replacementMap) {
		
		//First we take care of all the other fields,
		// their arguments must match ours
		
		if( this.numArgs() > 0 && this.getArg(0) instanceof Predicate )
			throw new RuntimeException( "Not yet implemented: non-Relation Predicates with Predicate args");
		

		
//		Predicate[] newSupers = null;
//		if( _supers != null ){ 
//			newSupers = new Predicate[_supers.length];
//			for( int i=0; i<_supers.length; i++ ){
//				newSupers[i] = _supers[i]._parent.recastCopy(replacementMap);
//			}
//		}
		
		//Skipping composedOf because I think we'll be removing that field
		
		Conjunction newDefinition = null;
		if( _definition != null )
			newDefinition = _definition.recastCopy(replacementMap);
		 
		Predicate[] newInferredFrom = null;
		if( _inferredFrom != null ){
			newInferredFrom = new Predicate[_inferredFrom.length];
			for( int i=0; i<_inferredFrom.length; i++ )
				newInferredFrom[i] = _inferredFrom[i].recastCopy(replacementMap);
		}
		 
		//Now take care of our argument replacement
		PredArg[] newArgs = new PredArg[numArgs()];
		for( int i=0; i<numArgs(); i++ ){
			if(  replacementMap.containsKey( getArg(i) )  ){
				newArgs[i] = replacementMap.get( getArg(i) ).copy();
				if(  newArgs[i].lessSpecificThan( getArg(i) )  )
					throw new RuntimeException("Can't cast PredArg to less specific type or varName: " +
							newArgs[i]+" cannot equal "+getArg(i) );
				 
			} else
			//FIXME: Lotta holes in this renaming I think, need to overhaul PredArg naming
			if(  replacementMap.containsValue( getArg(i) )  ){
				//Something else is being renamed to this
				// but no replacement for this is specified
				// so we need to rename this one (and rename
				// the things it can't equal ie ?x!?y
				newArgs[i] = getArg(i).copy();
				String oldName = newArgs[i].getVariableName().substring(1);
				newArgs[i].setVariableName("?skolem"+oldName);
				
				String[] toRename = newArgs[i].getCannotEqual();
				for( int j=0; j<toRename.length; j++ ){
					if(  replacementMap.containsKey( new PredArg(toRename[j],null) )  )
						toRename[j] = replacementMap.get( new PredArg(toRename[j],null) ).getVariableName().split("!")[0];
				}
				 
			} else {
				//Nothing is being renamed to this but still
				// no replacement is specified and in the future
				// there might be clashing with this variable name
				// so we'll rename it anyway 
				
				//copypasta
				newArgs[i] = getArg(i);
				String oldName = newArgs[i].getVariableName().substring(1);
				newArgs[i].setVariableName("?skolem"+oldName);
				
				String[] toRename = newArgs[i].getCannotEqual();
				for( int j=0; j<toRename.length; j++ ){
					if(  replacementMap.containsKey( new PredArg(toRename[j],null) )  )
						toRename[j] = replacementMap.get( new PredArg(toRename[j],null) ).getVariableName().split("!")[0];
				}
			}
		}
		
		Predicate copy = new Predicate(
				_name, 
				_supers,
				_composedOf,
				_definition,
				_inferredFrom, 
				newArgs);
			
		return copy;
	}
	
//	private static Map<String,String> mapFromString(String mapString){
//		Map<String,String> returnMap = new HashMap<String,String>();
//		
//		String[] keyValuePairs = mapString.split("|");
//		for( String keyValuePair : keyValuePairs){
//			String[] keyAndValue = keyValuePair.split(":");
//			
//			if( keyAndValue.length != 2 )
//				throw new RuntimeException("Improper map string: "+mapString);
//			
//			String key = keyAndValue[0];
//			String value = keyAndValue[1];
//			
//			returnMap.put(key, value);
//		}
//		
//		return returnMap;
//	}
	
	public String toString(){
		String string = _name+"(";
		for( PredArg arg : _args )
			string+=arg+",";
		if( numArgs() > 0 )
			string = string.substring(0, string.length()-1);
		string+=")";
		return string;
	}
	
	public static Predicate fromString(String string){
		String[] strings = string.split("\\(|:|,|\\)");
		
		PredArg[] args = new PredArg[(strings.length-1)/2];
		for( int i=0; i<(strings.length-1)/2; i++){
			String type = strings[i*2+1];
			String value = strings[i*2+2];
			if( type.equalsIgnoreCase("Location"))
				args[i] = new PredLocation(null, value);
			else if( type.equalsIgnoreCase("Object"))
				args[i] = new PredObject(null, value);
			else if( type.equalsIgnoreCase("Actor") )
				args[i] = new PredActor(null, value);
			else if( type.equalsIgnoreCase("Agent") )
				args[i] = new PredAgent(null, value);
		}
		
		return new Predicate(strings[0], args);
	}
	
	public List<PredicateInterval> findIn(ActivityInstance instance){
		return findIn(this,instance);
	}
	
	public List<PredicateInterval> findIn(Predicate predicate, ActivityInstance instance){
		
		List<PredicateInterval> resultIntervals = new ArrayList<PredicateInterval>();
		List<Bindings> resultBindings = new ArrayList<Bindings>();
//		System.out.println("a "+resultBindings);
		predicate.findIn(null, instance, null, resultIntervals, resultBindings);
//		System.out.println("b "+resultIntervals);
//		System.out.println("c "+resultBindings);
		for (int i = 0; i < resultIntervals.size(); i++) {
//			System.out.println("d "+resultBindings.get(i));
			Predicate c = predicate.copy();
			c.bindArguments(resultBindings.get(i));
			resultIntervals.get(i).setPredicate(c);
		}
		
		return resultIntervals;
	}
	
	public void findIn(Map<Predicate,PredicateInterval> found, ActivityInstance instance,
			Bindings currentBindings, List<PredicateInterval> resultIntervals, List<Bindings> resultBindings) {
		findIn(this, found, instance, currentBindings, resultIntervals, resultBindings);
	}
	
	public static void findIn(Predicate pattern, Map<Predicate,PredicateInterval> found, ActivityInstance instance,
			Bindings currentBindings, List<PredicateInterval> resultIntervals, List<Bindings> resultBindings) {
		

		if( found == null )
			found = new HashMap<Predicate,PredicateInterval>();
		
		found.put(pattern, null);
		
//		if( fluentsMap.containsKey(pattern.getName()) )
		
		//Look for predicates that extend from this predicate, and infer the existence of this one
		
		//Get all known predicates that extend from pattern
		List<Predicate> extendedTos = KnownPredicates.extendedTo.get(pattern.getName());
		if( extendedTos != null )
			//iterate through them
			for( Predicate extension : extendedTos )
				//if there are any fluents with these extension predicates
				if( instance.getIntervalsMap().containsKey(extension.getName()) ){
					//add a clone of said fluent with the predicate replaced with pattern
					for( PredicateInterval toClone : instance.getIntervalsMap().get(extension.getName()) ){
						//need to figure out how to bind unbound extension to bound one in fluent

						Bindings cloneBindings = extension.getValidBinding(toClone.getPredicate(), null);
						//frigging hell this is getting complicated
						for( int i=0; i<pattern.numArgs(); i++ ){
							String boundValue = cloneBindings.remove(extension.getArg(i));
							PredArg copy = extension.getArg(i);
							copy.setVariableName(pattern.getArg(i).getVariableName());
							cloneBindings.put(copy, boundValue);
						}
						
						instance.addInterval( clone(toClone, pattern.copy(), cloneBindings) );
				
					}
				}
		
		//TODO: Other inferences
		//For now if anything is on the inferredFrom list, we find any intervals with that
		//  predicate, and add a new interval for that duration, only with _this_ predicate
		if( pattern.getInferredFrom() != null)
			for( Predicate predicate : pattern.getInferredFrom() ){
				
				predicate.findIn(instance);
				
				if( instance.getIntervalsMap().get(predicate.getName()) != null )
					for( PredicateInterval interval : instance.getIntervalsMap().get(predicate.getName()) ){
						
						Bindings bindings = predicate.getValidBinding(interval.getPredicate(), null);
						
						// Assert that the binding is valid (two arguments refer to distinct objects)
						if (bindings != null && bindings.get(predicate.getArg(0)) != null ) {
							
							// Create new intention predicate from ddts predicate (for instance)
							// Add inferred intention to activity instance
							PredicateInterval pi = clone(interval, pattern.copy(), bindings);
							instance.addInterval( pi );
						}
						
					}
			}
		
		if( pattern.getDefinition() != null ){
			Conjunction definition = new Conjunction(pattern.getDefinition());
			
			List<Map<Predicate,PredicateInterval>> something = new ArrayList<Map<Predicate,PredicateInterval>>();
			recognize(definition, found, instance, currentBindings, something, resultBindings);
			
			for( Map<Predicate,PredicateInterval> map : something ){
				resultIntervals.add(  map.get( definition.list.get(definition.list.size()-1) )  );
			}
		}
		
	}
	
	static private void recognize(Conjunction pattern, Map<Predicate, PredicateInterval> found, 
			ActivityInstance instance, Bindings currentBindings, 
			List<Map<Predicate,PredicateInterval>> resultIntervals, List<Bindings> resultBindings) {

		if( pattern.list.isEmpty() ){
			resultIntervals.add(found);
			resultBindings.add(currentBindings);
			return;
		}
		
		if( found == null )
			found = new HashMap<Predicate,PredicateInterval>();
		
		//copy
		pattern = new Conjunction(pattern);
		Relation relation = pattern.list.remove(0);
//		System.out.println("what the "+relation);
		
		//relations only ever have 2 args
		Predicate arg0 = relation.getArg(0);
		Predicate arg1 = relation.getArg(1);
		List<PredicateInterval> intervalArgs0 = new ArrayList<PredicateInterval>();
		List<PredicateInterval> intervalArgs1 = new ArrayList<PredicateInterval>();
		
		if( found.containsKey(arg0) ){
			//We've already found a matching interval, use that
			
			intervalArgs0.add( found.get(arg0) );
			if( intervalArgs0.get(0) == null ) {
				// key is inserted with null as a placeholder, to say that we are already looking
				//   for this further up the recursion
				throw new RuntimeException(" definition contains circular references. " + arg0);
			}
			
		} else {
			//first arg not yet constrained, let's look for matches
			instance.addIntervals( arg0.findIn(instance) );
			
			//something about bindings here... TODO
			List<PredicateInterval> fluents = instance.getIntervalsMap().get(arg0.getName());
			if( fluents != null )
				intervalArgs0.addAll( fluents );
			
		}
		
		if( found.containsKey(arg1) ){
			//We've already found a matching interval, use that
			
			intervalArgs1.add( found.get(arg1) );
			if( intervalArgs1.get(0) == null ) {
				// key is inserted with null as a placeholder, to say that we are already looking
				//   for this further up the recursion
				throw new RuntimeException(" definition contains circular references. " + arg1);
			}
			
		} else {
			//first arg not yet constrained, let's look for matches
			instance.addIntervals( arg1.findIn(instance) );
			
			List<PredicateInterval> fluents = instance.getIntervalsMap().get(arg1.getName());
			if( fluents != null )
				intervalArgs1.addAll( fluents );
			
		}
		
		for( PredicateInterval intervalArg0 : intervalArgs0 ){
			if( arg0.bindableTo(intervalArg0.getPredicate(), currentBindings) ){//bindableTo calls getValidBinding so this is non-optimal
				
				/* Anh (08/10) - I think there's a bug here!  We are using currentBindings to check
				 * for the binding of arg0, yet we change it each time.  So after arg0 is sucessfully
				 * bounded the first time, this binding will prevent subsequent arg0's to be bounded.
				 * Changed to using a new variable to store the new binding containing the bound arg0.
				 */
//				currentBindings = arg0.getValidBinding(intervalArg0.getPredicate(), currentBindings);
				Bindings boundArg0 = arg0.getValidBinding(intervalArg0.getPredicate(), currentBindings);
				
				for( PredicateInterval intervalArg1 : intervalArgs1 ) {
					if( arg1.bindableTo(intervalArg1.getPredicate(), boundArg0) ) {
						
						PredicateInterval relationInterval = relation.getInterval(intervalArg0, intervalArg1);
						
						if( relationInterval != null ){
							//these intervals do have that relation
							
							//getValidBindings returns a copy, so the same one won't be passed on to the next recursion
//							Bindings validBindings = arg0.getValidBinding(intervalArg0.getPredicate(), currentBindings);
//							Bindings validBindings = arg1.getValidBinding(intervalArg1.getPredicate(), currentBindings);
							Bindings boundArg0Arg1 = arg1.getValidBinding(intervalArg1.getPredicate(), boundArg0);
							
							//may need to copy found first
							found.put(arg0, intervalArg0);
							found.put(arg1, intervalArg1);
							found.put(relation, relationInterval);

							recognize(pattern, new HashMap<Predicate,PredicateInterval>(found), instance, boundArg0Arg1, resultIntervals, resultBindings);
							
						}
					
					}
				}
			}
		}
		
	}
	
	private static PredicateInterval clone( PredicateInterval interval, Predicate replacement, Bindings bindings ){
		
		PredicateInterval clone = interval.copy();
		replacement.bindArguments(bindings);
		clone.setPredicate(replacement);
		
		return clone;
	}
	
	public static void test(){
		
//		Predicate has1 = new Predicate("has", new PredActor(null, "vendingMachine1"), new PredObject(null, "Snickers1"));
//		Predicate hates1 = new Predicate("hates", new PredAgent("?x",null), new PredObject(null,"vendingMachine1"));
//		
//		System.out.println(has1 + " " + hates1);
//		
//		Predicate hates2 = new Predicate("hates", new PredAgent(null,"guy1"), new PredObject(null,"vendingMachine1"));
//		System.out.println(hates1.bindableTo(hates2, null));
//		
//		Predicate hates3 = new Predicate("hates", new PredObject(null,"box1"), new PredObject(null,"vendingMachine1"));
//		System.out.println(hates1.bindableTo(hates3, null));
//		
//		Predicate has2 = has1.copy();
//		has2.setName("wants");
//		
//		System.out.println(has1 + " " + has2);
//		
//		System.out.println(  has1.toString() + " " + fromString( has1.toString() )  );
		
		
		
	}
	
	public static void main(String[] args){
		test();
	}
	
	@Override
	public int hashCode(){
		return System.identityHashCode(this);
	}
	
	@Override
	public boolean equals(Object object){
		return this.hashCode() == object.hashCode();
	}
	
}